package com.fzf.org.toolkit;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fzf.org.dto.net.RedisInfoDTO;
import com.fzf.org.exception.BizException;
import com.fzf.org.exception.BizExceptionTypeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.fzf.org.toolkit.ConstantKey.*;

@Slf4j
@Component
@RequiredArgsConstructor
public class RedisOperateService {

    private static final ObjectMapper objectMapper = new ObjectMapper();
    private final StringRedisTemplate stringRedisTemplate;

    private static final String TOKEN_BLACK_ZSET = "token:black:zset";

    private static final String LOGIN_EXPIRE_PREFIX = "login:expire:";

    private static final String LOGIN_FAIL_KEY_PREFIX = "login:fail:";
    private static final String LOGIN_INFO_PREFIX = "login:info:";


    /**
     * 返回当前1分钟内 redis中记录的某个用户登录失败次数
     *
     * @param account 用户账号
     * @return 当前已经失败登录的次数
     */
    public long getFailCount(String prefix, String account) {
        String redisKey = LOGIN_FAIL_KEY_PREFIX + prefix + ":" + account;
        long time = System.currentTimeMillis();
        Long count = stringRedisTemplate.opsForZSet().count(redisKey, time - 61 * 1000, Double.POSITIVE_INFINITY);
        return null == count ? 0L : count;
    }


    /**
     * @param account 用户账号
     * @return 当前已经失败登录的次数
     */
    public long getAndSetFailCount(String prefix, String account) {
        String redisKey = LOGIN_FAIL_KEY_PREFIX + prefix + ":" + account;
        long time = System.currentTimeMillis();
        stringRedisTemplate.opsForZSet().add(redisKey, account + time, time);
        Long count = stringRedisTemplate.opsForZSet().count(redisKey, time - 61 * 1000, Double.POSITIVE_INFINITY);
        stringRedisTemplate.expire(redisKey, 61, TimeUnit.SECONDS);
        return null == count ? 0L : count;
    }


    /**
     * 在redis中删除记录某个用户登录失败的次数
     *
     * @param account 用户账号
     */
    public void deleteFailCount(String prefix, String account) {
        String redisKey = LOGIN_FAIL_KEY_PREFIX + prefix + ":" + account;
        stringRedisTemplate.delete(redisKey);
    }


    /**
     * 每当用户登录成功之后，需要将一些用户信息以及成功生成的JWT token存放到redis中，方便后续用户拿取信息以及
     * 管理员对每个用户进行实时的踢蹬操作处理
     *
     * @param prefix 用于区分admin与普通用户的前缀
     * @param account      用户的账号
     * @param redisInfoDTO 用户的redis中存储的信息实体
     */
    public void setInfoWithExpireTIme(String prefix, String account, RedisInfoDTO redisInfoDTO) {
        String key = LOGIN_INFO_PREFIX + prefix + ":" + account;
        try {
            String value = objectMapper.writeValueAsString(redisInfoDTO);
            stringRedisTemplate.opsForValue().set(key,
                    value, INFO_EXPIRE_TIME, TimeUnit.SECONDS);
        } catch (JsonProcessingException e) {
            //
            throw new BizException(BizExceptionTypeEnum.SERVICE_ERROR);
        }
    }

    /**
     *
     * @param prefix 用于区分admin与普通用户的前缀
     * @param account 户的账号
     */
    public void deleteInfo(String prefix, String account) {
        String key = LOGIN_INFO_PREFIX + prefix + ":" + account;
        stringRedisTemplate.delete(key);
    }

    /**
     * 根据用户id查询存储在redis中用户信息，如果不存在返回null，若存在则正常转为用户信息实体返回
     *
     * @param prefix 用于区分admin与普通用户的前缀
     * @param account 用户的账号
     * @return 用户存储在redis中用户信息
     */
    public RedisInfoDTO getInfo(String prefix, String account) {
        String key = LOGIN_INFO_PREFIX + prefix + ":" + account;
        String value = stringRedisTemplate.opsForValue().get(key);
        if (null == value) {
            return null;
        } else {
            try {
                return objectMapper.readValue(value, RedisInfoDTO.class);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 每当一个用户第一次成功登录之后，redis会在一个zset结构中存储一条记录：用户id作为value，登陆的过期时间作为score，
     * 这样做的目的是配合定时任务修改用户的登出状态，避免出现状态与行为的不一致
     *
     * @param prefix 用于区分admin与普通用户的前缀
     * @param account 用户账号
     */
    public void addToLoginExpireZset(String prefix, String account) {
        String redisKey = LOGIN_EXPIRE_PREFIX + prefix;
        stringRedisTemplate.opsForZSet().add(redisKey, account, System.currentTimeMillis() + TOKEN_EXPIRE_TIME_MILLIS);
    }

    /**
     * 从记录登录过期时间中的zset删除掉某个用户数据，这是当用户主动进行登出时进行的操作
     *
     * @param prefix 用于区分admin与普通用户的前缀
     * @param account 用户账号
     */
    public void removeFromLoginExpireZSet(String prefix, String account) {
        String redisKey = LOGIN_EXPIRE_PREFIX + prefix;
        Long remove = stringRedisTemplate.opsForZSet().remove(redisKey, account);
    }

    /**
     * 返回已经于当前时间之前登录token 失效的用户id列表，用于对用户登录状态的批量修改
     *
     * @param prefix 用于区分admin与普通用户的前缀
     * @return 需要修改状态的用户id列表
     */
    public List<String> getAndRemoveFromLoginExpireZSetByRange(String prefix) {
        String redisKey = LOGIN_EXPIRE_PREFIX + prefix;
        long time = System.currentTimeMillis();
        Set<String> keySet = stringRedisTemplate.opsForZSet().rangeByScore(redisKey, 0, time);
        stringRedisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, time);

        if (null == keySet || keySet.isEmpty()) {
            return null;
        } else {
            return new ArrayList<>(keySet);
        }
    }

    /**
     * 将已经登出或者被管理员踢出的token放入黑名单中，每次检验token之前先判断token是否在黑名单中，若在直接允许登录
     * 若不在才对token进行校验
     *
     * @param token JWT 生成的token
     */
    public void addTokenToBlackTokenZset(String token) {
        stringRedisTemplate.opsForZSet().add(TOKEN_BLACK_ZSET, token, System.currentTimeMillis() + TOKEN_EXPIRE_TIME_MILLIS);
    }

    /**
     * 返回当前token是否存在于黑名单中
     *
     * @param token JWT 生成的token
     * @return 返回token是否存在于黑名单
     */
    public boolean isTokenInBlackTokenZset(String token) {
        return stringRedisTemplate.opsForZSet().score(TOKEN_BLACK_ZSET, token) != null;
    }

    /**
     * 定期清理黑名单中的token
     */
    public void removeTokenFromBlackTokenZset() {
        stringRedisTemplate.opsForZSet().removeRangeByScore(TOKEN_BLACK_ZSET, 0, System.currentTimeMillis());
    }

}
